Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Definition
struct MyStruct1 {}
class MyChildClass {}
//Usage
Console.WriteLine("MyStruct1 is a struct? {0}", typeof (ValueType)
.IsAssignableFrom(typeof (MyStruct1))); // True
Console.WriteLine("MyStruct1 is a object? {0}", typeof (object)
.IsAssignableFrom(typeof (MyStruct1))); // True
Console.WriteLine("MyChildClass is a struct? {0}", typeof (ValueType)
.IsAssignableFrom(typeof (MyChildClass))); // False
Console.WriteLine("MyChildClass is a object? {0}", typeof (object)
.IsAssignableFrom(typeof (MyChildClass))); // True
In the above diagram, MyStruct1 is inherited from System.ValueType and MyChildClass is inherited from MyBaseClass, and MyBaseClass itself inherits from Class. Both Class and System.ValueType are inherited from Object Class. Since both MyStruct1 and MyChildClass are inherited from Object Class we can transition (change) between these two.
We call this feature Variance. It makes reference transitions between related types through inheritance. In short, we can say Variance is a change between types.
In C# programming language, we make the transition between types using Variance feature in three ways.
Invariance feature: The type to be assigned and the type assigned must be the same. This means that there is no switch between a Child (More Derived) type and a Parent (Less Derived) type.
Covariance feature: You can switch from a Child type to a Parent type.
More Derived -> Less Derived transition
Contravariance feature: You can switch from a Parent type to a Child type.
Less Derived -> More Derived transition
As we frequently use the definitions of More Derived and Less Derived within the subject, let us clarify them.
Code:
1 2 3
class Base {} class Derived: Base {} class Derived2: Derived {}
Let’s go step by step as the related description may change according to the direction we look at.
Object is the ancestor of all types, it is always the Less Derived type compared to other types.
Base type is More Derived according to Object type. So Derived is Less Derived type according to Derived2.
The Derived type is More Derived according to Object and Base types, Less Derived type according to Derived2 type.
We assign types every time we write programs. For example:
1 2 3 4 5
string value1 = "value1"; //Normal assignment object value2 = "value2"; //More Derived to Less Derived assignment Console.WriteLine($ "{value1}"); //value1 Console.WriteLine($ "{value2}"); //value2 Console.WriteLine($ "value2 is a {value2.GetType().Name}."); //value2 is a String
We just assigned String types variable into Object type. Because String type is actually inherited from Object type. Well, what if we perform the assignment in the opposite manner?
1
2
3
4
string value1 = "value1"; //Normal assignment
object value2 = "value2"; //More Derived to Less Derived assignment
//ERROR !!
string value3 = value2; // Less Derived to More Derived assignment
We get an error. The Object cannot be implicitly converted to String type.
If we know that the Object(value2) is actually an object that can be converted to a String type, we can cast an explicit way and eliminate the error.
1
string value3 = (string) value2; // string => string assignment
But when we cast types like this, we tell the system that the Object is really a String. If it is not a String object, we get an error during the runtime.
For example:
1
2
object value1 = new object();
string value2 = (string) value1; // object => string assignment, runtime error
If you have noticed, we can assign More derived types to Less derived types without any problems. But when we want to do the opposite (for example, object to string assignment), we encounter a problem. If we can say that the type (assignment.GetType() )
to be assigned is actually the same type or more derived as the assigned object, we can do this with the cast operation. In short, assignments in C# world work with covariant mode.
All parameters we use with this method is work as Covariant compatible. For example:
let’s send different types of values to a method that accepts an object type parameter and prints the result to the screen.
1
2
3
4
5
6
7
8
9
10
11
12
13
//Class Definition
class AClass {}
//Method Definition
void CallMe(object obj) {
Console.WriteLine($ "Type is {obj.GetType().Name}. Value is {obj}");
}
//Usage
CallMe(100); //Type is Int32. Value is 100
CallMe(100 f); //Type is Single. Value is 100
CallMe("Hello"); //Type is String. Value is Hello
CallMe(new AClass()); //Type is AClass. Value is AClass
Using Covariance, we converted all types to Object types and used them.
Arrays and collections show covariance in reference types and invariance in value types. But type-safe does not work.
Let’s make an example of a string of reference types.
1
2
3
4
object[] array1 = new string[3]; // 1. step
array1[0] = ""; // 2. step
array1[1] = 5; // 3. step
array1[2] = new object(); // 4. step
Let’s examine it step by step.
Step 1: We assign the string type to the object type. (Covariance feature)
Step 2: We assign a string type value to the first element of the array.
Covariance property string => object assignment
Since array1 is actually a string type, there is no problem.
Step 3: We assign an int value to the second element of the array.
Covariance property int => object assignment
Since array1 is actually an array of type string, it cannot take an element of type int. (not-type-safe, ERROR)
Step 4: We assign an object type value to the third element of the array.
Covariance property object => object assignment
Since array1 is actually an array of type string, it cannot take an element of type object. (not-type-safe, ERROR)
When we run it, we get an ArrayTypeMismatchException error.
1
2
3
object[] array1 = new string[3];
array1[0] = “”;
array1[1] = 5; // Exception Unhandled Error
It was invariant for value types. For example:
1
2
//Compile error
object[] array1 = new int[3];
We get a compilation error. An array of type value cannot be assigned to the object type.
1
object[] array1 = new int[3]; // readonly struct System.Int32
Because collections like ArrayList work with object [], they have the same problem. So they are not type-safe.
By default, Generic interfacers display invariance. In other words, the Generic passed type must be the same as the generic type to be assigned.
1 2 3 4 5 6 7 8 9 10 11 12
interface IExample < T > {} //Invariance approach IExample < int > example = (IExample < int > ) null; //Line 1 IExample < string > example2 = (IExample < string > ) null; //Line 2 //Covariance approach IExample < object > example3 = (IExample < string > ) null; //Line 3, Compiler Error //Contravariance approach IExample < string > example4 = (IExample < object > ) null; //Line 4, Compiler Error
Since they are invariant, they must be of the same type. Briefly, the assignment is appropriate for Line 1 and Line 2, the assignment was not made for Line 3 and Line 4.
(Note: This situation can be changed. We will review in detail later.)
Generic collections were of the invariant type because they used generic interfaces as their infrastructure. However, with C# version 4.0, covariant and contravariant types are features that are provided.
For example, the following code gives a compile error in C# below version 4.0, but in the later version it works, also it’s type-safe.
1 2 3
IEnumerable < object > enumerable = new List < string > (); //Before C# 4.0 Compiler Error IEnumerable < object > enumerable = new List < string > (); //After C# 4.0 OK, type-safe
Stackoverflow page: https://stackoverflow.com/a/53134957/8069766
C# | Object Class. https://www.geeksforgeeks.org/c-sharp-object-class/
Covariance and Contravariance (C#). https://docs.microsoft.com/tr-tr/dotnet/csharp/programming-guide/concepts/covariance-contravariance/
Covariance and Contravariance in Generics. https://docs.microsoft.com/tr-tr/dotnet/standard/generics/covariance-and-contravariance